#pragma once

#include "thirdparty/lua-5.3.5/src/lua.hpp"
#include <google/protobuf/message.h>
#include <cstdint>
#include <tuple>
#include <initializer_list>
#include <type_traits>
#include <utility>
#include <string>
#include "lua_framework.hh"

template <typename T>
inline void luaPush(lua_State* state, T arg) {
}

template <>
inline void luaPush(lua_State* state, std::int16_t arg) {
    lua_pushinteger(state, (lua_Integer)arg);
}

template <>
inline void luaPush(lua_State* state, std::uint16_t arg) {
    lua_pushinteger(state, (lua_Integer)arg);
}

template <>
inline void luaPush(lua_State* state, std::int32_t arg) {
    lua_pushinteger(state, (lua_Integer)arg);
}

template <>
inline void luaPush(lua_State* state, std::int64_t arg) {
    lua_pushinteger(state, (lua_Integer)arg);
}

template <>
inline void luaPush(lua_State* state, std::uint32_t arg) {
    lua_pushinteger(state, (lua_Integer)arg);
}

template <>
inline void luaPush(lua_State* state, std::uint64_t arg) {
    lua_pushinteger(state, (lua_Integer)arg);
}

template <>
inline void luaPush(lua_State* state, bool arg) {
    lua_pushboolean(state, arg ? 1 : 0);
}

template <>
inline void luaPush(lua_State* state, const char* arg) {
    lua_pushstring(state, arg);
}

template <>
inline void luaPush(lua_State* state, float arg) {
    lua_pushnumber(state, (lua_Number)arg);
}

template <>
inline void luaPush(lua_State* state, double arg) {
    lua_pushnumber(state, (lua_Number)arg);
}

struct PushMethod {
    lua_State* L_;
    PushMethod(lua_State* L) {
        L_ = L;
    }
    void operator()(std::int16_t& v) {
        luaPush(L_, v);
    }
    void operator()(std::uint16_t& v) {
        luaPush(L_, v);
    }
    void operator()(std::int32_t& v) {
        luaPush(L_, v);
    }
    void operator()(std::int64_t& v) {
        luaPush(L_, v);
    }
    void operator()(std::uint32_t& v) {
        luaPush(L_, v);
    }
    void operator()(std::uint64_t& v) {
        luaPush(L_, v);
    }
    void operator()(float& v) {
        luaPush(L_, v);
    }
    void operator()(double& v) {
        luaPush(L_, v);
    }
    void operator()(bool& v) {
        luaPush(L_, v);
    }
    void operator()(const char* v) {
        luaPush(L_, v);
    }
};

template <class Tuple, class F, std::size_t... I>
constexpr F foreach_impl(Tuple&& t, F&& f, std::index_sequence<I...>) {
    return (void)std::initializer_list<int>{(std::forward<F>(f)(std::get<I>(std::forward<Tuple>(t))),0)...}, f;
}

template <class Tuple, class F>
constexpr F tuple_foreach(Tuple&& t, F&& f) {
    return foreach_impl(std::forward<Tuple>(t), std::forward<F>(f),
        std::make_index_sequence<std::tuple_size<std::remove_reference_t<Tuple>>::value>{});
}

class StackBalancer {
    lua_State* state_;
    int top_;
    bool balanced_;
public:
    StackBalancer(lua_State* state) {
        state_ = state;
        top_ = lua_gettop(state_);
        balanced_ = false;
    }

    ~StackBalancer() {
        if (!balanced_) {
            lua_settop(state_, top_);
        }
    }

    void balance() {
        lua_settop(state_, top_);
        balanced_ = true;
    }

    void cancel() {
        balanced_ = true;
    }
};

extern std::string getStackInfo(lua_State* L);
extern void installCModule(lua_State* L, const char* name, const luaL_Reg* clibFunc);
extern bool installRPCMethod(lua_State* L, const std::string& callPath, lua_CFunction cfunc);
extern void uninstallRPCMethod(lua_State* L, const std::string& interface);
extern void installCoTable(lua_State* L);
extern bool catchError(lua_State* L, int status);
extern void raiseError(lua_State* L, const char* msg);

struct CoInfo {
    int ref_;
    lua_State* co_;
    lua_State* resumer_;

    CoInfo(lua_State* co) = delete;
    CoInfo(const CoInfo& rht) = delete;
    CoInfo(CoInfo&& rht) = delete;
    const CoInfo& operator=(const CoInfo&) = delete;
    static CoInfo* newThread();
    static void deleteThread(CoInfo* coi);
    CoInfo();
    ~CoInfo();
    void clear();
    inline operator lua_State*() {
        return co_;
    }
    inline operator bool() {
        return (co_ != nullptr);
    }
    inline int ref() {
        return ref_;
    }
    inline lua_State* resumer() {
        return resumer_;
    }
    inline void setResumer(lua_State* resumer) {
        resumer_ = resumer;
    }
};

inline void pushBoolToLua(lua_State* co, const google::protobuf::Message& msg, const google::protobuf::FieldDescriptor* field) {
    auto value = msg.GetReflection()->GetBool(msg, field);
    lua_pushboolean(co, value);
    lua_setfield(co, -2, field->name().c_str());
}

inline void pushInt32ToLua(lua_State* co,const google::protobuf::Message& msg, const google::protobuf::FieldDescriptor* field) {
    auto value = msg.GetReflection()->GetInt32(msg, field);
    lua_pushinteger(co, (lua_Integer)value);
    lua_setfield(co, -2, field->name().c_str());
}

inline void pushUInt32ToLua(lua_State* co,const google::protobuf::Message& msg, const google::protobuf::FieldDescriptor* field) {
    auto value = msg.GetReflection()->GetUInt32(msg, field);
    lua_pushinteger(co, (lua_Integer)value);
    lua_setfield(co, -2, field->name().c_str());
}

inline void pushInt64ToLua(lua_State* co,const google::protobuf::Message& msg, const google::protobuf::FieldDescriptor* field) {
    auto value = msg.GetReflection()->GetInt64(msg, field);
    lua_pushinteger(co, (lua_Integer)value);
    lua_setfield(co, -2, field->name().c_str());
}

inline void pushUInt64ToLua(lua_State* co,const google::protobuf::Message& msg, const google::protobuf::FieldDescriptor* field) {
    auto value = msg.GetReflection()->GetUInt64(msg, field);
    lua_pushinteger(co, (lua_Integer)value);
    lua_setfield(co, -2, field->name().c_str());
}

inline void pushFloatToLua(lua_State* co,const google::protobuf::Message& msg, const google::protobuf::FieldDescriptor* field) {
    auto value = msg.GetReflection()->GetFloat(msg, field);
    lua_pushnumber(co, (lua_Number)value);
    lua_setfield(co, -2, field->name().c_str());
}

inline void pushDoubleToLua(lua_State* co,const google::protobuf::Message& msg, const google::protobuf::FieldDescriptor* field) {
    auto value = msg.GetReflection()->GetDouble(msg, field);
    lua_pushnumber(co, (lua_Number)value);
    lua_setfield(co, -2, field->name().c_str());
}

inline void pushEnumToLua(lua_State* co,const google::protobuf::Message& msg, const google::protobuf::FieldDescriptor* field) {
    auto value = msg.GetReflection()->GetEnum(msg, field);
    lua_pushinteger(co, (lua_Integer)value->number());
    lua_setfield(co, -2, field->name().c_str());
}

inline void pushStringToLua(lua_State* co,const google::protobuf::Message& msg, const google::protobuf::FieldDescriptor* field) {
    auto value = msg.GetReflection()->GetString(msg, field);
    lua_pushstring(co, value.c_str());
    lua_setfield(co, -2, field->name().c_str());
}

inline void setFieldString(lua_State* co, google::protobuf::Message & msg, const google::protobuf::Reflection* ref, const google::protobuf::FieldDescriptor* field, int index) {
    if (lua_isstring(co, index)) {
        ref->SetString(&msg, field, lua_tostring(co, index));
    } else {
        errorlog << "Message field type mismatch for " << field->name() << ", need a string" << endlog;
    }
}

inline void setFieldInt32(lua_State* co, google::protobuf::Message & msg, const google::protobuf::Reflection* ref, const google::protobuf::FieldDescriptor* field, int index) {
    if (lua_isinteger(co, index)) {
        ref->SetInt32(&msg, field, (std::int32_t)lua_tointeger(co, index));
    } else {
        errorlog << "Message field type mismatch for " << field->name() << ", need a int32" << endlog;
    }
}

inline void setFieldInt64(lua_State* co, google::protobuf::Message & msg, const google::protobuf::Reflection* ref, const google::protobuf::FieldDescriptor* field, int index) {
    if (lua_isinteger(co, index)) {
        ref->SetInt64(&msg, field, (std::int64_t)lua_tointeger(co, index));
    } else {
        errorlog << "Message field type mismatch for " << field->name() << ", need a int64" << endlog;
    }
}

inline void setFieldUInt32(lua_State* co, google::protobuf::Message & msg, const google::protobuf::Reflection* ref, const google::protobuf::FieldDescriptor* field, int index) {
    if (lua_isinteger(co, index)) {
        ref->SetUInt32(&msg, field, (std::uint32_t)lua_tointeger(co, index));
    } else {
        errorlog << "Message field type mismatch for " << field->name() << ", need a uint32" << endlog;
    }
}

inline void setFieldUInt64(lua_State* co, google::protobuf::Message & msg, const google::protobuf::Reflection* ref, const google::protobuf::FieldDescriptor* field, int index) {
    if (lua_isinteger(co, index)) {
        ref->SetUInt64(&msg, field, (std::uint64_t)lua_tointeger(co, index));
    } else {
        errorlog << "Message field type mismatch for " << field->name() << ", need a uint64" << endlog;
    }
}

inline void setFieldBool(lua_State* co, google::protobuf::Message & msg, const google::protobuf::Reflection* ref, const google::protobuf::FieldDescriptor* field, int index) {
    if (lua_isboolean(co, index)) {
        ref->SetBool(&msg, field, lua_toboolean(co, index) ? true : false);
    } else {
        errorlog << "Message field type mismatch for " << field->name() << ", need a boolean" << endlog;
    }
}

inline void setFieldFloat(lua_State* co, google::protobuf::Message & msg, const google::protobuf::Reflection* ref, const google::protobuf::FieldDescriptor* field, int index) {
    if (lua_isnumber(co, index)) {
        ref->SetFloat(&msg, field, (float)lua_tonumber(co, index));
    } else {
        errorlog << "Message field type mismatch for " << field->name() << ", need a number" << endlog;
    }
}

inline void setFieldDouble(lua_State* co, google::protobuf::Message & msg, const google::protobuf::Reflection* ref, const google::protobuf::FieldDescriptor* field, int index) {
    if (lua_isnumber(co, index)) {
        ref->SetDouble(&msg, field, (double)lua_tonumber(co, index));
    } else {
        errorlog << "Message field type mismatch for " << field->name() << ", need a number" << endlog;
    }
}

inline void setFieldEnum(lua_State* co, google::protobuf::Message & msg, const google::protobuf::Reflection* ref, const google::protobuf::FieldDescriptor* field, int index) {
    if (lua_isinteger(co, index)) {
        auto num = (int)lua_tointeger(co, index);
        ref->SetEnumValue(&msg, field, num);
    } else {
        errorlog << "Message field type mismatch for " << field->name() << ", need a integer" << endlog;
    }
}

inline void addFieldInt32(lua_State* co, google::protobuf::Message & msg, const google::protobuf::Reflection* ref, const google::protobuf::FieldDescriptor* field, int index) {
    if (lua_isinteger(co, index)) {
        ref->AddInt32(&msg, field, (std::int32_t)lua_tointeger(co, index));
    } else {
        errorlog << "Message field type mismatch for " << field->name() << ", need a int32" << endlog;
    }
}

inline void addFieldInt64(lua_State* co, google::protobuf::Message & msg, const google::protobuf::Reflection* ref, const google::protobuf::FieldDescriptor* field, int index) {
    if (lua_isinteger(co, index)) {
        ref->AddInt64(&msg, field, (std::int64_t)lua_tointeger(co, index));
    } else {
        errorlog << "Message field type mismatch for " << field->name() << ", need a int64" << endlog;
    }
}

inline void addFieldUInt32(lua_State* co, google::protobuf::Message & msg, const google::protobuf::Reflection* ref, const google::protobuf::FieldDescriptor* field, int index) {
    if (lua_isinteger(co, index)) {
        ref->AddUInt32(&msg, field, (std::uint32_t)lua_tointeger(co, index));
    } else {
        errorlog << "Message field type mismatch for " << field->name() << ", need a uint32" << endlog;
    }
}

inline void addFieldUInt64(lua_State* co, google::protobuf::Message & msg, const google::protobuf::Reflection* ref, const google::protobuf::FieldDescriptor* field, int index) {
    if (lua_isinteger(co, index)) {
        ref->AddUInt64(&msg, field, (std::uint64_t)lua_tointeger(co, index));
    } else {
        errorlog << "Message field type mismatch for " << field->name() << ", need a uint64" << endlog;
    }
}

inline void addFieldString(lua_State* co, google::protobuf::Message & msg, const google::protobuf::Reflection* ref, const google::protobuf::FieldDescriptor* field, int index) {
    if (lua_isstring(co, index)) {
        ref->AddString(&msg, field, lua_tostring(co, index));
    } else {
        errorlog << "Message field type mismatch for " << field->name() << ", need a string" << endlog;
    }
}

inline void addFieldBool(lua_State* co, google::protobuf::Message & msg, const google::protobuf::Reflection* ref, const google::protobuf::FieldDescriptor* field, int index) {
    if (lua_isboolean(co, index)) {
        ref->AddBool(&msg, field, lua_toboolean(co, index) ? true : false);
    } else {
        errorlog << "Message field type mismatch for " << field->name() << ", need a boolean" << endlog;
    }
}

inline void addFieldEnum(lua_State* co, google::protobuf::Message & msg, const google::protobuf::Reflection* ref, const google::protobuf::FieldDescriptor* field, int index) {
    if (lua_isinteger(co, index)) {
        ref->AddEnumValue(&msg, field, (int)lua_tointeger(co, index));
    } else {
        errorlog << "Message field type mismatch for " << field->name() << ", need a integer" << endlog;
    }
}

inline void addFieldFloat(lua_State* co, google::protobuf::Message & msg, const google::protobuf::Reflection* ref, const google::protobuf::FieldDescriptor* field, int index) {
    if (lua_isnumber(co, index)) {
        ref->AddFloat(&msg, field, (float)lua_tonumber(co, index));
    } else {
        errorlog << "Message field type mismatch for " << field->name() << ", need a number" << endlog;
    }
}

inline void addFieldDouble(lua_State* co, google::protobuf::Message & msg, const google::protobuf::Reflection* ref, const google::protobuf::FieldDescriptor* field, int index) {
    if (lua_isnumber(co, index)) {
        ref->AddDouble(&msg, field, (double)lua_tonumber(co, index));
    } else {
        errorlog << "Message field type mismatch for " << field->name() << ", need a number" << endlog;
    }
}

extern void toLua(lua_State* co, const google::protobuf::Message& msg);

extern void copyTableLuaToC(lua_State* co, google::protobuf::Message & msg);

extern void onRepeatedField(lua_State* co, const google::protobuf::Message& msg, const google::protobuf::FieldDescriptor* field);

extern void onField(lua_State* co, const google::protobuf::Message& msg, const google::protobuf::FieldDescriptor* field);

extern void onLuaRepeatedValue(lua_State* co, google::protobuf::Message & msg, const google::protobuf::Reflection* ref, const google::protobuf::FieldDescriptor* field, int index);

extern void onLuaTableValue(lua_State* co, google::protobuf::Message & msg, const google::protobuf::Reflection* ref, const google::protobuf::FieldDescriptor* field, int index);
extern void onCallAck(lua_State* co, const google::protobuf::Message& msg);
extern void onCallTimeout(lua_State* co);